package soundTest;
// Example 17-3. SoundPlayer.java

import java.io.*;
import java.net.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.sound.sampled.*;
import javax.sound.midi.*;

/**
 * This class is a Swing component that can load and play a sound clip,
 * displaying progress and controls.  The main( ) method is a test program.
 * This component can play sampled audio or MIDI files, but handles them 
 * differently. For sampled audio, time is reported in microseconds, tracked in
 * milliseconds and displayed in seconds and tenths of seconds. For midi
 * files time is reported, tracked, and displayed in MIDI "ticks".
 * This program does no transcoding, so it can only play sound files that use
 * the PCM encoding.
 */
public class SoundPlayer extends JComponent {
    boolean midi;            // Are we playing a midi file or a sampled one?
    Sequence sequence;       // The contents of a MIDI file
    Sequencer sequencer;     // We play MIDI Sequences with a Sequencer
    Clip clip;               // Contents of a sampled audio file
    boolean playing = false; // whether the sound is currently playing

    // Length and position of the sound are measured in milliseconds for 
    // sampled sounds and MIDI "ticks" for MIDI sounds
    int audioLength;         // Length of the sound.  
    int audioPosition = 0;   // Current position within the sound

    // The following fields are for the GUI
    JButton play;             // The Play/Stop button
    JSlider progress;         // Shows and sets current position in sound
    JLabel time;              // Displays audioPosition as a number
    Timer timer;              // Updates slider every 100 milliseconds

    // The main method just creates a SoundPlayer in a Frame and displays it
    public static void main(String[  ] args) 
        throws IOException,
               UnsupportedAudioFileException,
               LineUnavailableException,
               MidiUnavailableException,
               InvalidMidiDataException
    {
        SoundPlayer player;

        File file = new File(args[0]);   // This is the file we'll be playing
        // Determine whether it is midi or sampled audio
        boolean ismidi;
        try {
            // We discard the return value of this method; we just need to know
            // whether it returns successfully or throws an exception
            MidiSystem.getMidiFileFormat(file);
            ismidi = true;
        }
        catch(InvalidMidiDataException e) {
            ismidi = false;
        }

        // Create a SoundPlayer object to play the sound.
        player = new SoundPlayer(file, ismidi);

        // Put it in a window and play it
        JFrame f = new JFrame("SoundPlayer");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane( ).add(player, "Center");
        f.pack( );
        f.setVisible(true);
    }

    // Create a SoundPlayer component for the specified file.
    public SoundPlayer(File f, boolean isMidi)
        throws IOException,
               UnsupportedAudioFileException,
               LineUnavailableException,
               MidiUnavailableException,
               InvalidMidiDataException
    {
        if (isMidi) {     // The file is a MIDI file
            midi = true;
            // First, get a Sequencer to play sequences of MIDI events
            // That is, to send events to a Synthesizer at the right time.
            sequencer = MidiSystem.getSequencer( );  // Used to play sequences
            sequencer.open( );                       // Turn it on.

            // Get a Synthesizer for the Sequencer to send notes to
            Synthesizer synth = MidiSystem.getSynthesizer( );
            synth.open( );  // acquire whatever resources it needs
            
            // The Sequencer obtained above may be connected to a Synthesizer
            // by default, or it may not.  Therefore, we explicitly connect it.
            Transmitter transmitter = sequencer.getTransmitter( );
            Receiver receiver = synth.getReceiver( );
            transmitter.setReceiver(receiver);
            
            // Read the sequence from the file and tell the sequencer about it
            sequence = MidiSystem.getSequence(f);
            sequencer.setSequence(sequence);
            audioLength = (int)sequence.getTickLength( ); // Get sequence length
        }
        else {            // The file is sampled audio
            midi = false;
            // Getting a Clip object for a file of sampled audio data is kind
            // of cumbersome.  The following lines do what we need.
            AudioInputStream ain = AudioSystem.getAudioInputStream(f);
            try {
                DataLine.Info info =
                    new DataLine.Info(Clip.class,ain.getFormat( ));
                clip = (Clip) AudioSystem.getLine(info);
                clip.open(ain);
            }
            finally { // We're done with the input stream.
                ain.close( );
            }
            // Get the clip length in microseconds and convert to milliseconds
            audioLength = (int)(clip.getMicrosecondLength( )/1000);
        }

        // Now create the basic GUI
        play = new JButton("Play");                // Play/stop button
        progress = new JSlider(0, audioLength, 0); // Shows position in sound
        time = new JLabel("0");                    // Shows position as a #

        // When clicked, start or stop playing the sound
        play.addActionListener(new ActionListener( ) {
                public void actionPerformed(ActionEvent e) {
                    if (playing) stop( ); else play( );
                }
            });

        // Whenever the slider value changes, first update the time label.
        // Next, if we're not already at the new position, skip to it.
        progress.addChangeListener(new ChangeListener( ) {
                public void stateChanged(ChangeEvent e) {
                    int value = progress.getValue( );
                    // Update the time label
                    if (midi) time.setText(value + "");
                    else time.setText(value/1000 + "." +
                                      (value%1000)/100);
                    // If we're not already there, skip there.
                    if (value != audioPosition) skip(value);
                }
            });
        
        // This timer calls the tick( ) method 10 times a second to keep 
        // our slider in sync with the music.
        timer = new javax.swing.Timer(100, new ActionListener( ) {
                public void actionPerformed(ActionEvent e) { tick( ); }
            });
        
        // put those controls in a row
        Box row = Box.createHorizontalBox( );
        row.add(play);
        row.add(progress);
        row.add(time);
        
        // And add them to this component.
        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
        this.add(row);

        // Now add additional controls based on the type of the sound
        if (midi) addMidiControls( );
        else addSampledControls( );
    }

    /** Start playing the sound at the current position */
    public void play( ) {
        if (midi) sequencer.start( );
        else clip.start( );
        timer.start( );
        play.setText("Stop");
        playing = true;
    }

    /** Stop playing the sound, but retain the current position */
    public void stop( ) {
        timer.stop( );
        if (midi) sequencer.stop( );
        else clip.stop( );
        play.setText("Play");
        playing = false;
    }

    /** Stop playing the sound and reset the position to 0 */
    public void reset( ) {
        stop( );
        if (midi) sequencer.setTickPosition(0);
        else clip.setMicrosecondPosition(0);
        audioPosition = 0; 
        progress.setValue(0);
    }

    /** Skip to the specified position */
    public void skip(int position) { // Called when user drags the slider
        if (position < 0 || position > audioLength) return;
        audioPosition = position;
        if (midi) sequencer.setTickPosition(position);
        else clip.setMicrosecondPosition(position * 1000);
        progress.setValue(position); // in case skip( ) is called from outside
    }

    /** Return the length of the sound in ms or ticks */
    public int getLength( ) { return audioLength; }

    // An internal method that updates the progress bar.
    // The Timer object calls it 10 times a second.
    // If the sound has finished, it resets to the beginning
    void tick( ) {
        if (midi && sequencer.isRunning( )) {
            audioPosition = (int)sequencer.getTickPosition( );
            progress.setValue(audioPosition);
        }
        else if (!midi && clip.isActive( )) {
            audioPosition = (int)(clip.getMicrosecondPosition( )/1000);
            progress.setValue(audioPosition);
        }
        else reset( );  
    }

    // For sampled sounds, add sliders to control volume and balance
    void addSampledControls( ) {
        try {
            FloatControl gainControl =
                (FloatControl)clip.getControl(FloatControl.Type.MASTER_GAIN);
            if (gainControl != null) this.add(createSlider(gainControl));
        }
        catch(IllegalArgumentException e) {
            // If MASTER_GAIN volume control is unsupported, just skip it
        }

        try {
            // FloatControl.Type.BALANCE is probably the correct control to
            // use here, but it doesn't work for me, so I use PAN instead.
            FloatControl panControl =
                (FloatControl)clip.getControl(FloatControl.Type.PAN);
            if (panControl != null) this.add(createSlider(panControl));
        }
        catch(IllegalArgumentException e) {  }
    }


    // Return a JSlider component to manipulate the supplied FloatControl
    // for sampled audio.
    JSlider createSlider(final FloatControl c) {
        if (c == null) return null;
        final JSlider s = new JSlider(0, 1000);
        final float min = c.getMinimum( );
        final float max = c.getMaximum( );
        final float width = max-min;
        float fval = c.getValue( );
        s.setValue((int) ((fval-min)/width * 1000));

        java.util.Hashtable labels = new java.util.Hashtable(3);
        labels.put(new Integer(0), new JLabel(c.getMinLabel( )));
        labels.put(new Integer(500), new JLabel(c.getMidLabel( )));
        labels.put(new Integer(1000), new JLabel(c.getMaxLabel( )));
        s.setLabelTable(labels);
        s.setPaintLabels(true);

        s.setBorder(new TitledBorder(c.getType( ).toString( ) + " " +
                                     c.getUnits( )));

        s.addChangeListener(new ChangeListener( ) {
                public void stateChanged(ChangeEvent e) {
                    int i = s.getValue( );
                    float f = min + (i*width/1000.0f);
                    c.setValue(f);
                }
            });
        return s;
    }

    // For Midi files, create a JSlider to control the tempo,
    // and create JCheckBoxes to mute or solo each MIDI track.
    void addMidiControls( ) {
        // Add a slider to control the tempo
        final JSlider tempo = new JSlider(50, 200);
        tempo.setValue((int)(sequencer.getTempoFactor( )*100));
        tempo.setBorder(new TitledBorder("Tempo Adjustment (%)"));
        java.util.Hashtable labels = new java.util.Hashtable( );
        labels.put(new Integer(50), new JLabel("50%"));
        labels.put(new Integer(100), new JLabel("100%"));
        labels.put(new Integer(200), new JLabel("200%"));
        tempo.setLabelTable(labels);
        tempo.setPaintLabels(true);
        // The event listener actually changes the tempo
        tempo.addChangeListener(new ChangeListener( ) {
                public void stateChanged(ChangeEvent e) {
                    sequencer.setTempoFactor(tempo.getValue( )/100.0f);
                }
            });

        this.add(tempo);

        // Create rows of solo and checkboxes for each track
        Track[  ] tracks = sequence.getTracks( );
        for(int i = 0; i < tracks.length; i++) {
            final int tracknum = i;
            // Two checkboxes per track
            final JCheckBox solo = new JCheckBox("solo");
            final JCheckBox mute = new JCheckBox("mute");
            // The listeners solo or mute the track
            solo.addActionListener(new ActionListener( ) {
                    public void actionPerformed(ActionEvent e) {
                        sequencer.setTrackSolo(tracknum,solo.isSelected( ));
                    }
                });
            mute.addActionListener(new ActionListener( ) {
                    public void actionPerformed(ActionEvent e) {
                        sequencer.setTrackMute(tracknum,mute.isSelected( ));
                    }
                });

            // Build up a row
            Box box = Box.createHorizontalBox( );
            box.add(new JLabel("Track " + tracknum));
            box.add(Box.createHorizontalStrut(10));
            box.add(solo);
            box.add(Box.createHorizontalStrut(10));
            box.add(mute);
            box.add(Box.createHorizontalGlue( ));
            // And add it to this component
            this.add(box);
        }
    }
}
